第三章 程序的机器级表示

time will tell Lv4

在本章我们主要通过汇编代码来理解程序在计算机的底层运行

3.4 访问信息

3.4.1 操作数指示符

1730211778218

3.4.2 数据传送指令

mov

第一条指令将源值加载到寄存器中,第二条将该寄存器值写人目的位

3.4.4 压入和弹出栈数据

在x86-64中,程序栈存放在内存中某个区域。如图3-9所示,向下增长,这样
来,栈顶元素的地址是所有栈中元素地址中最低的。(根据惯例,我们的是倒过来画的
栈“顶”在图的底部。)栈指针%rsp保存着栈顶元素的地址。

1730213176025

将一个四字值压人栈中,首先要将栈指针减8,然后将值写到新的栈顶地址。因此,
指令 pushq %rbp 的行为等价于下面两条指令:

1730254023111

它们之间的区别是在机器代码中pushg指令编码为1个字节,而上面那两条指令一共需要
8个字节。图中前两栏给出的是,当%rsp为0x108,%rax为0x123时,执行指令pushg %rax的效果。首先%rsp会减8,得到0x100,然后会将0x123存放到内存地址0x100处。

1730254244932

栈操作说明。根据惯例,我们的栈是倒过来画的,因而栈“顶”在底部。x86-64中,
栈向低地址方向增长,所以压栈是减小栈指针(寄存器%rsp)的值,并将数据存放到
内存中,而出栈是从内存中读数据,并增加栈指针的值

根据gpt的生成,有一个更通俗的解释: %rsp 是栈指针寄存器,它始终指向栈的顶部。当你压入一个新元素时,栈指针会减小,指向新的栈顶;当你弹出一个元素时,栈指针则会增大,返回到之前的栈顶位置。

3.5 算术和逻辑操作

1730254720260

整数算术操作。加载有效地址(1eag)指令通常用来执行简单的算术操作。其余的指令
是更加标准的一元或二元操作。我们用>>A和>>L来分别表示算术右移和逻辑右移。
注意,这里的操作顺序与ATT格式的汇编代码中的相反

3.5.1 加载有效地址

加载有效地址(load effective address)指令 leag实际上是 movg指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读人数据,而是将有效地址写人到目的操作数。

实际用法

简单地址计算
1
2
leaq (%rbx, %rcx), %rax

这条指令会将 %rbx + %rcx 的值存入 %rax。常用于计算两个寄存器的和。

加上偏移量
1
2
leaq 8(%rbx), %rax

%rbx + 8 的值存入 %rax,即将 %rbx 地址偏移 8 个字节。常用于数组或结构体字段的偏移计算。

带比例因子的地址计算
1
2
leaq (%rbx, %rcx, 4), %rax

%rbx + %rcx * 4 的值存入 %rax,其中 4 是比例因子。典型应用是访问结构体数组元素或做指针运算。

计算复杂的地址
1
2
leaq 16(%rbx, %rcx, 2), %rax

%rbx + %rcx * 2 + 16 的值存入 %rax。这种写法可以处理多层偏移与比例因子,应用广泛。

3.5.2 一元和二元操作

在上表的第二组中,只有一个操作数,其既是源又是目的。

第三组是二元操作,其中,第二个操作数既是源又是目的。

3.5.3 移位操作

最后一组是移位操作,先给出移位量,然后第二项给出的是要移位的数,第二项既是源又是目的。

3.5.5 特殊的算术操作

两个64位有符号或无符号整数相乘得到的乘积需要128位来表示。x86-64指令集对128位(16字节)数的操作提供有限的支持。延续字(2字节)、双字(4 字节)和四字(8字节)的命名惯例,Intel把16字节的数称为八字(oct word)。

1730255802807

在乘法函数

1
2
3
void store_uprod(uint128_t *dest,uint64_t x,uint64_t y){
*dest=x*(uint128_t)y;
}

解析的gcc汇编代码中

1730256319861

操作2将x复制到乘法版

根据mul的隐式操作数:

mulq 是一种无符号乘法指令,它的操作数分为被乘数和乘数:

  • 被乘数:在指令执行前,mulq 指令默认使用寄存器 %rax 中的值作为被乘数。
  • 乘数:指令中的操作数(在这个例子中是 %rdx)被视为乘数。

mulq %rdx 指令的含义是将 %rax 的值(被乘数)与 %rdx 的值(乘数)相乘,此时无论操作数是什么,乘法的结果会存储在 %rax(低 64 位)和 %rdx(高 64 位)中

因此,操作3和操作4的目的就是将乘法的高低位结果取出,放到x中

与之相对应的除法中,也有类似的一元运算

idivq %rsi 是 x86-64 汇编中的一个有符号除法指令。它使用 %rsi 作为除数,对 %rdx:%rax 组成的 128 位被除数进行除法运算。

而cqto作为辅助除法计算的指令,用于将寄存器 %rax 的符号位扩展到 %rdx。这在执行有符号除法(如 idivq)之前是必要的,因为 idivq 需要一个 128 位的被除数,而这个被除数是由 %rdx:%rax 组合而成的。

3.6 控制

3.6.1 条件码

除了整数寄存器,CPU还维护着一组单个位的条件码(conditioncode)寄存器,它们描述了最近的算术或逻辑操作的属性。可以检测这些寄存器来执行条件分支指令。最常用的条件码有:

CF(Carry Flag,进位标志)

  • 功能 :当无符号运算结果产生进位或借位时设置。
  • 示例add 指令在无符号加法产生进位时设置 CF。

ZF(Zero Flag,零标志)

  • 功能 :当结果为零时设置。
  • 示例cmp 指令比较两个值,如果它们相等,则设置 ZF。

OF(Overflow Flag,溢出标志)

  • 功能 :当有符号运算结果溢出时设置。
  • 示例add 指令在有符号加法产生溢出时设置 OF。

SF(Sign Flag,符号标志)

  • 功能 :当结果为负数时设置。
  • 示例sub 指令在结果为负数时设置 SF。

就ADD指令而言,完成一个t=a+b的功能,根据下面C表达式来设置条件码

1730257938831

注意:leaq指令不改变任何条件码,因为它是用来进行地址计算的。

3.6.2 访问条件码

条件码通常不会直接读取,常用的使用方法有三种:

1)可以根据条件码的某种组合将一个字节设置为0或者1,

2)可以条件跳转到程序的某个其他的部分,

3)可以有条件地传送数据。

指令名字的不同后缀指明了它们所考虑的条件码的组合,而这些指令的后缀表示不同的条件而不是操作数大小

以下是SET指令。每条指令根据条件码的某种组合,将一个字节设置为0或者1。
有些指令有“同义名”,也就是同一条机器指令有别的名字

1730258369298

示例:

setl %al

  • 功能 :根据条件码设置 %al%rax 的低 8 位)。
  • 操作 :如果比较结果为“有符号小于”(SF ≠ OF),则将 %al 设置为 1;否则,将 %al 设置为 0。

1730258766851

3.6.3 跳转指令

跳转(jump)指令可以执行切换到程序中的另外一个位置。在汇编代码中,跳转的目的地通常用一个符号(label)表明,如:

1730258891725

指令 jmp .L1会导致程序跳过 movq指令,而从popq指令开始继续执行。在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。

同时,jmp指令是无条件跳转,即它可以是直接跳转,即跳转目标是作为指令的一部分编码的;也可以是间接跳转,即跳转目标是从寄存器或内存位置中读出的。

指令
jmp *%rax
用寄存器%rax中的值作为跳转目标

而指令
jmp *(%rax)
以%rax中的值作为读地址,从内存中读出跳转目标

1730259548877

表中所示的其他跳转指令都是有条件的–它们根据条件码的某种组合,或者跳转,或者继续执行代码序列中下一条指令。这些指令的名字和跳转条件与SET指令的名字和设置条件是相匹配的。同SET指令一样,一些底层的机器指令有多个名字。
条件跳转只能是直接跳转。

3.6.4 跳转指令的编码

下面是一个PC相对寻址的例子,这个函数的汇编代码由编译文件branch.c产生。它包含两个跳转:第2行的jmp指令前向跳转到更高的地址,而第7行的jg指令后向跳转到较低的地址。

1730263221556

这些例子说明,当执行PC相对寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。这种惯例可以追溯到早期的实现,当时的处理器会将更新程序计数器作为执行一条指令的第一步。

3.6.5 用条件控制来实现条件分支

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
long lt_cnt =0;
long ge_cnt =0;
long absdiff_se(long x, long y)
{
long result;
if(x< y){
lt_cnt++;
result =y-x;
else {
ge_cnt++;
result =x-y;
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
long gotodiff_se(long x,long y)
{
long result;
if(x >= y)
goto x_ge_y;
lt_cnt++;
result=y-x;
return result;
x_ge_y:
ge_cnt++;
result =x-y;
return result;
}

1730269901824

3.6.6 用条件传送来实现条件分支

控制的条件转移是当条件满足时,程序沿着一条执行路径执行,而当条件不满足时,就走另一条路径,但是在现代处理器上,它可能非常低效。

一种替代的策略是使用数据的条件转移。这种方法计算一个条件操作的两种结果,然后再根据条件是否满足从中选取一个。只有在一些受限制的情况中,这种策略才可行,但是如果可行,就可以用一条简单的条件传送指令来实现它。

前面的例子中,分支里有副作用,会修改 lt_cnt或ge_cnt的值,而这个版本只是简单地计算函数要返回的值。

1
2
3
4
5
6
7
8
9
10
11
12
long cmovdiff(long x,long y)
{
long rval =y-x;
long eval = x-y;
long ntest =x>= y;
/* Line below requires
single instruction:
*/
if(ntest)rval=eval;
return rval;
}

1730271361620

处理器通过使用流水线(pipelining)来获得高性能,在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操作的一小部分(例如,从内存取指令、确定指令类型、从内存读数据、执行算术运算、向内存写数据,以及更新程序计数器),这种方法通过重叠连续指令的步骤来获得高性能。

当机器遇到条件跳转(也称为“分支”)时,只有当分支条件求值完成之后,才能决定分支往哪边走。

但是在另一方面,错误预测一个跳转要求处理器丢掉它为该跳转指令后所有指令已做的工作,然后再开始用从正确位置处起始的指令去填充流水线。正如我们会看到的,这样一个错误预测会招致很严重的惩罚,浪费大约15~30个时钟周期,导致程序性能严重下降。

另一方面,无论测试的数据是什么,编译出来使用条件传送的代码所需的时间都是大约8个时钟周期。控制流不依赖于数据,这使得处理器更容易保持流水线是满的。

3-18

列举了x86-64上一些可用的条件传送指令。每条指令都有两个操作数:源寄存器或者内存地址S,和目的寄存器R。与不同的SET(3.6.2节)和跳转指令(3.6.3节)一样,这些指令的结果取决于条件码的值。源值可以从内存或者源寄存器中读取,但是只有在指定的条件满足时,才会被复制到目的寄存器中。

1730271829448

同条件跳转不同,处理器无需预测测试的结果就可以执行条件传送。处理器只是读源值(可能是从内存中),检查条件码,然后要么更新目的寄存器,要么保持不变。

3.6.7 循环

1.do-while语句

1730272060085通用形式被翻译为如下

1
2
3
4
5
loop:
body-statement
t= test-expr;
if(t)
goto loop;

1730272290711

2.while循环
1
2
3
4
5
6
7
    goto test;
loop:
body-statement
test:
t = test-expr;
if(t)
goto loop;

1730272377329

3.for循环

跳转到中间策略的goto代码:

1
2
3
4
5
6
7
8
goto test;
loop:
body-statement
update-expr ;
test:
t= test-expr;
if(t)
goto loop;

而guarded-do策略得到

1
2
3
4
5
6
7
8
9
10
    t= test-expr;
if(!t)
goto done;
loop:
body-statement
update-expr;
t= test-expr;
if(t)
goto loop;
done:

1730286050357

3.6.8 switch语句

switch(开关)语句可以根据一个整数索引值进行多重分支(multiway branching)。

和使用组很长的if-else语句相比,使用跳转表的优点是执行开关语句的时间与开关情况的量无关。

17302862211941730286232624

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
section .text
global switch_eg

switch_eg:
subq $100, %rsi ; Compute index = n - 100
cmpq $6, %rsi ; Compare index with 6
ja loc_def ; If index > 6, goto loc_def

leaq .L4(,%rsi,8), %rax ; Compute address of jump table entry
jmp *%rax ; Jump to the corresponding case

.L4:
.quad loc_A ; Case 0: loc_A
.quad loc_B ; Case 1: loc_B
.quad loc_C ; Case 2: loc_C
.quad loc_D ; Case 3: loc_D
.quad loc_def ; Case 4: loc_def
.quad loc_def ; Case 5: loc_def
.quad loc_def ; Case 6: loc_def

loc_A:
leaq (%rdi,%rdi,2), %rax ; val = 3 * x
leaq (%rdi,%rax,4), %rdi ; val = 13 * x
jmp done

loc_B:
addq $10, %rdi ; x = x + 10
movq %rdi, %rax ; val = x
jmp done

loc_C:
addq $11, %rdi ; val = x + 11
movq %rdi, %rax
jmp done

loc_D:
imulq %rdi, %rdi ; val = x * x
movq %rdi, %rax
jmp done

loc_def:
movl $0, %edi ; val = 0
movq %rdi, %rax

done:
movq %rax, (%rdx) ; *dest = val
ret

3.7 过程

要提供对过程的机器级支持,必须要处理许多不同的属性。为了讨论方便,假设过程P调用过程Q,Q执行后返回到P。这些动作包括下面一个或多个机制:
传递控制。在进人过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址。
传递数据。P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
分配和释放内存。在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间。

3.7.1 运行时栈

当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧(stackfram)。

大多数过程的栈帧都是定长的,在过程的开始就分配好了,但是总会遇到需要变长的栈

汇编代码指令(指南)

提前说明,指令后面带的字母表示操作位数

b、w、l、q分别代表Byte(8位)、Word(16位)、Long(32位)、Quadword(64位),其余情况相同

还有一个重要说明

x86汇编中汇编指令遵循“目标,源”的顺序,也就是说,第一个参数是存储结果的位置,第二个参数是操作的数字

在 Linux 汇编(基于 AT&T 语法的 x86-64 汇编)中,操作数的顺序确实与其他汇编风格有所不同。 AT&T 语法使用的是“源,目标”顺序 ,因此指令的第一个参数是源操作数,第二个参数是目标操作数。这个顺序与 Intel 风格的“目标,源”是相反的。

下面的指令不再过多赘述

1、subq

在汇编语言中,subq 是 x86-64 架构中用于执行整数减法的指令。这里的 q 代表“quadword”,即操作 64 位(8 字节)的数据。subq 指令用于从一个操作数中减去另一个操作数,并将结果存储在第一个操作数中。

1
subq src, dest
  • src : 源操作数,表示要减去的值。
  • dest : 目标操作数,表示要从中减去值的地方,减法结果会存储在这里。

注意事项

  • 在使用 subq 指令时,需要确保目标操作数的值足够大,以避免出现下溢(即负数的情况)。无符号数的操作会忽略负数表示。
  • 汇编语言通常不提供高级别的错误处理机制,因此要谨慎处理可能的溢出情况。

2、imulq

imulq指令用来执行两个寄存器存储的变量之间的乘法

imulq 指令的基本语法如下:

1
2
imulq destination, source

  • destination : 乘法结果存放的位置。通常是一个寄存器或内存地址。
  • source : 要乘的值,可以是寄存器或内存地址。

该命令将 destination 中的值与 source 中的值相乘,并将结果存储回 destination

3、mov指令

在 x86 和 x86-64 汇编语言中,mov 指令有多种变体,允许不同的操作数类型和大小。以下是一些常见的 mov 指令变式及其用法:

1. 基本变体
  • mov 指令 : 用于将数据从一个位置移动到另一个位置。
1
2
mov destination, source

2. 数据大小
  • movb : 移动 8 位(1 字节)的数据。
1
2
3
movb al, [var]      ; 将内存中的 1 字节数据移动到 al 寄存器
movb [var], bl ; 将 bl 寄存器中的 1 字节数据存储到内存

  • movw : 移动 16 位(2 字节)的数据。
1
2
3
movw ax, [var]      ; 将内存中的 2 字节数据移动到 ax 寄存器
movw [var], bx ; 将 bx 寄存器中的 2 字节数据存储到内存

  • movl : 移动 32 位(4 字节)的数据。
1
2
3
movl eax, [var]     ; 将内存中的 4 字节数据移动到 eax 寄存器
movl [var], ebx ; 将 ebx 寄存器中的 4 字节数据存储到内存

  • movq : 移动 64 位(8 字节)的数据。
1
2
3
movq rax, [var]     ; 将内存中的 8 字节数据移动到 rax 寄存器
movq [var], rbx ; 将 rbx 寄存器中的 8 字节数据存储到内存

1729775130152

但是作为零扩展字节,传送与传送之间亦有区别

1729843014057

3. 立即数
  • 将立即数加载到寄存器 :
1
2
3
mov eax, 42         ; 将立即数 42 加载到 eax 寄存器
mov rax, 0xFF ; 将 0xFF 加载到 rax 寄存器

4. 不同操作数类型
  • 寄存器与寄存器之间 :
1
2
3
mov eax, ebx        ; 将 ebx 的值复制到 eax
mov rax, rbx ; 将 rbx 的值复制到 rax

  • 内存与寄存器之间 :
1
2
3
mov [var], eax      ; 将 eax 的值存储到内存
mov eax, [var] ; 将内存中的值加载到 eax

5. 内存寻址
  • 使用偏移量 :
1
2
3
mov eax, [rbx + 8]  ; 从内存地址 rbx + 8 处读取数据到 eax
mov [rbx + 8], ecx ; 将 ecx 的值存储到内存地址 rbx + 8

  • 使用基址和索引 :
1
2
3
mov eax, [rbx + rsi] ; 从内存地址 rbx + rsi 处读取数据到 eax
mov [rbx + rsi], ecx ; 将 ecx 的值存储到内存地址 rbx + rsi

6. 注意事项
  • 不同大小的操作数 : mov 指令在操作数大小方面是严格的,不能直接将不同大小的数据移动。例如,不能将 32 位的数据直接移动到 64 位的寄存器,除非进行适当的扩展(如使用 movzxmovsx 指令进行零扩展或符号扩展)。
  • 影响标志位 : mov 指令不会影响任何 CPU 标志位,它只是简单地执行数据复制。

4、sal指令

SAL(Shift Arithmetic Left,算术左移)指令在 x86 汇编语言中用于将一个寄存器或内存中的二进制数向左移动若干位,并且用 0 填充被移出的低位。SAL 指令与 SHL(Shift Left,逻辑左移)是等效的。SAL 指令通常用于乘以 2 的幂,因为每次左移一位,相当于乘以 2。

1
2
sal destination, count

  • destination : 进行左移操作的目标操作数,通常是寄存器或内存位置。
  • count : 左移的位数,可以是立即数或 CL 寄存器的值。
影响标志位

SAL 指令会影响以下 CPU 标志位:

  • CF (Carry Flag) : 如果最高位移出,则设置。
  • ZF (Zero Flag) : 如果移位结果为 0,则设置。
  • SF (Sign Flag) : 如果移位结果是负数(即最高位为 1),则设置。
  • OF (Overflow Flag) : 如果移位导致符号位改变(即负数变为正数或反之),则设置。
SHL 的关系
  • SALSHL 等效 : SALSHL 指令在 x86 汇编中没有区别。两者的作用相同,都是进行逻辑左移,通常没有符号相关的区别,因此可以互换使用。

5、sar指令

SAR(Shift Arithmetic Right,算术右移)指令在 x86 汇编语言中用于将一个寄存器或内存中的有符号数向右移若干位。与普通的逻辑右移指令 SHR 不同,SAR 会保留符号位的值(即最左边的位)来填充移位后的高位,确保结果仍然保留符号。SAR 通常用于将有符号整数除以 2 的幂。

1
2
sar destination, count

SAR 与 SHR 的区别
  • SAR : 算术右移,保留符号位,适用于有符号整数。
  • SHR : 逻辑右移,不保留符号位,适用于无符号整数。

6、XOR指令

XOR(按位异或)指令对两个操作数进行逐位异或操作。对于每一位,如果两个位不同,则结果为 1;如果相同,则结果为 0。基本逻辑如下:

A B A XOR B
0 0 0
0 1 1
1 0 1
1 1 0
1
2
xor destination, source

常见用途
  • 清零 : XOR 常用于将寄存器置为 0,例如 xor eax, eax,这样高效且不影响标志位。
  • 反转位 : 通过与一个掩码进行异或操作,可以反转特定位。
  • 简单加密 : 异或运算常用于加密算法,因为对同一个数据两次异或相同的密钥将还原原始数据。
1
2
3
4
5
xor eax, eax       ; 将 eax 寄存器清零,等价于 mov eax, 0
mov ebx, 5
xor ebx, 2 ; 将 ebx 与 2 进行异或操作,结果存放在 ebx 中
; 若 ebx = 5(二进制 0101),2 为(二进制 0010),则结果 ebx = 7(二进制 0111)

7、OR指令

OR(按位或)指令对两个操作数进行逐位或操作。对于每一位,如果两个位中有一个是 1,则结果为 1;如果两个都为 0,则结果为 0。

8、and指令

按位与操作

9、ret指令

return的缩写 ,RET(Return,返回)指令用于从当前的子程序(或函数)返回到调用该子程序的代码处。RET 指令将控制权交还给调用者,同时将程序计数器设置为调用点的下一条指令,以便程序继续执行。

1
2
3
ret            ; 无操作数的基本形式
ret n ; 带操作数的形式

10、jmp指令

JMP(Jump,跳转)指令用于无条件跳转到指定的地址,从而改变程序的执行流程。JMP 指令不会检查任何条件,也不依赖标志位,因此可以直接跳转到指定的目标位置继续执行。

1
2
jmp target

JMP 指令可以分为以下几种类型,具体取决于目标地址的表示方式:

  1. 短跳转(Short Jump)
    • 用于跳转到当前位置的 ±128 字节以内,节省指令大小。
    • 格式:jmp short target
  2. 近跳转(Near Jump)
    • 用于跳转到同一代码段内的任意位置。
    • 格式:jmp target
  3. 远跳转(Far Jump)
    • 用于跳转到不同的代码段(通常在操作系统和裸机开发中使用)。
    • 格式:jmp far segment:offset
jmp的常见用途
  1. 循环跳转
    在循环结构中使用 JMP 来跳回到循环的开始。

    1
    2
    3
    4
    start:
    ; 代码
    jmp start ; 跳转回到标签 start

  2. 跳转到子程序(或函数)

    JMP 实现调用和返回机制。在一些低级嵌入式编程中,JMP 可以用于跳转到一个函数的开头。

    1
    2
    3
    4
    5
    jmp my_function
    my_function:
    ; 函数代码
    ret

  3. 条件跳转

    可以与条件指令结合使用。虽然条件跳转(例如 JEJNE)更常用于此目的,但 JMP 也可以在条件判断后执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cmp eax, ebx
    je equal
    jmp not_equal
    equal:
    ; 代码
    jmp end
    not_equal:
    ; 代码
    end:
    ; 代码

11、cmp和je、jne

cmp返回两个值的bool结果,相当于字符串中的strcmp,在汇编程序中起到比较的作用。在跳转的情况下,je与之对应,起到条件分支的作用,而jne(jump not equal)等同于je的反向以下是详细说明:

JE(Jump if Equal)指令在 x86 汇编语言中是一种条件跳转指令,用于在两个值相等时跳转到指定位置。通常结合 CMP(Compare)指令使用,CMP 比较两个操作数,并设置标志寄存器的标志位。JE 会检查 ZF(Zero Flag,零标志位),只有当 ZF 被设置(即比较结果相等时)才会执行跳转。

1
2
3
cmp operand1, operand2
je target

JE 的工作原理
  • CMP 指令会比较两个操作数,operand1 - operand2 的结果不会被存储,但会影响标志寄存器。
  • 如果 operand1 等于 operand2,则 ZF(Zero Flag)被设置为 1,此时 JE 指令会跳转到目标位置。
  • 如果 operand1 不等于 operand2ZF 为 0,JE 指令不会跳转,程序会继续执行下一条指令。

12、test指令

test 指令用于 按位与 (AND)操作两个操作数,以检查特定位的状态,而不改变操作数的值。test 的结果仅影响 CPU 的标志位,如零标志位(ZF)、符号标志位(SF)、溢出标志位(OF)、进位标志位(CF)等。其主要用途是在进行条件跳转前进行快速判断。(?)

1
2
test operand1, operand2

常见用法示例
  1. 检查寄存器是否为零

    1
    2
    3
    4
    mov eax, 1      ; 将 1 放入 eax
    test eax, eax ; 测试 eax 是否为零
    je is_zero ; 如果为零,跳转到 is_zero

    • test eax, eaxeax 与自身按位与,若 eax 为零,则 ZF 被设置,je 指令跳转。
    • 这种方式比 cmp eax, 0 更简洁有效。
  2. 测试特定位是否为 1

    1
    2
    3
    4
    mov eax, 5       ; 二进制:00000000 00000000 00000000 00000101
    test eax, 4 ; 二进制:00000000 00000000 00000000 00000100
    jnz bit_is_set ; 如果测试结果非零,跳转到 bit_is_set

    • 这段代码检查 eax 中第 3 位是否为 1。
    • 如果为 1,test 的结果非零,ZF 未被设置,则 jnz 跳转。
  3. 检查偶数或奇数

    1
    2
    3
    4
    mov eax, 5       ; 假设 eax 为 5
    test eax, 1 ; 检查最低位
    jz is_even ; 如果结果为零,说明是偶数

    通过 test eax, 1 来检查 eax 的最低位。如果最低位为 0,ZF 被设置,表示 eax 是偶数。

总结
  • test 指令用于条件判断,快速检测特定位、寄存器是否为零。
  • 不改变操作数值,只影响标志位。

13、rep指令

在 x86 汇编中,REP 前缀用于重复执行紧跟的字符串操作指令,直到满足特定条件。REP 常用于批量处理数组或内存块,例如拷贝、填充和比较操作。REP 前缀与 MOVSB/MOVSW/MOVSDSTOSB/STOSW/STOSDLODSB/LODSW/LODSDSCASB/SCASW/SCASD 等字符串指令配合使用。

基本用法
  • REP:重复执行指令,直到 ECX(或 CX 在 16 位模式中)变为零。
  • REPE / REPZ:在 ZF(Zero Flag)为 1 且 ECX 不为 0 的情况下重复执行指令。
  • REPNE / REPNZ:在 ZF 为 0 且 ECX 不为 0 的情况下重复执行指令。
常见指令和用法示例
  1. REP MOVSB/MOVSW/MOVSD :用于复制一块内存数据

    1
    2
    3
    4
    5
    6
    mov ecx, 100          ; 复制 100 字节
    mov esi, src ; 源地址指针
    mov edi, dest ; 目标地址指针
    cld ; 清除方向标志位,确保地址递增
    rep movsb ; 复制 [esi] 到 [edi],重复执行 100 次

    • src 所指向的源地址的数据逐字节复制到 dest,共 ECX 个字节。
    • rep movsb[ESI] 的内容复制到 [EDI],然后分别递增 ESIEDI,直到 ECX 为零。
  2. REP STOSB/STOSW/STOSD :用于将相同的值填充到内存块中

    1
    2
    3
    4
    5
    mov ecx, 100          ; 设置重复次数为 100
    mov edi, buffer ; 设置目标地址
    mov al, 0 ; 要填充的值
    rep stosb ; 将 AL 中的值写入 [EDI],重复 100 次

    rep stosbAL 中的值写到 [EDI] 指向的内存位置,共 ECX 个字节,然后 ECX 递减。

  3. REPE SCASB/SCASW/SCASD :用于在内存块中搜索指定值

    1
    2
    3
    4
    5
    mov ecx, 100          ; 设置最大搜索范围为 100 个字节
    mov edi, buffer ; 设置搜索的开始地址
    mov al, 5 ; 要查找的字节值
    repe scasb ; 查找 AL 中的值,直到找到或 ECX 为 0

    repe scasb[EDI] 中查找 AL 的值,如果找到了并且 ZF = 1,则继续查找,否则停止。

  4. REPNE SCASB :用于在内存块中查找第一个不匹配的值

    1
    2
    3
    4
    5
    mov ecx, 100          ; 设置搜索的最大长度
    mov edi, buffer ; 设置开始搜索的地址
    mov al, 0 ; 要查找的不匹配值
    repne scasb ; 在 [EDI] 中查找非 AL 的值

    repne scasb 继续搜索,直到找到 [EDI] 中第一个与 AL 不匹配的值,或直到 ECX 变为零。

end:退出逻辑

在 Linux 系统中,每个系统调用都有一个编号。exit 系统调用的编号是 60eax 寄存器存放系统调用编号。

在程序

1
2
3
4
mov eax, 60           ; syscall: exit
xor edi, edi ; exit code 0
syscall

中,我们逐步解析退出的实现逻辑

逐行分析
  1. mov eax, 60
  • 将数字 60 加载到 eax,表示要调用 exit 系统调用来退出程序。
  • 这一步告知操作系统“我想要退出程序”。
  1. xor edi, edi
  • edi 是传递 exit 系统调用参数的寄存器,在这里用于传递退出状态码。
  • 通过 xor edi, edi 设置 edi 为 0,将退出状态码设为 0,表示 正常退出 (退出码 0 通常表示没有错误发生)。
  • xor 是一种优化的清零方法,比直接 mov edi, 0 更快,因此常用于清零操作。
  1. syscall
  • syscall 指令触发系统调用,这时 CPU 切换到内核模式,将程序的控制权交给操作系统。
  • 操作系统根据 eax 中的值(60)识别为 exit 系统调用,读取 edi 的值(0)作为退出码。
  • 内核执行 exit 系统调用,将当前程序终止,退出码传回给父进程。

通过这三行代码,程序完成了系统调用的设置并成功退出。

  • Title: 第三章 程序的机器级表示
  • Author: time will tell
  • Created at : 2024-10-24 18:05:09
  • Updated at : 2024-10-30 23:36:43
  • Link: https://sbwrn.github.io/2024/10/24/第三章 程序的机器级表示/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
第三章 程序的机器级表示